﻿// -----------------------------------------------------------------------
// <copyright file="RefTarget.cs" company="JSC Centido">
//  Copyright (c) JSC Centido. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

using System.Data.Odbc;

namespace Centido.Core
{
    using System;


    /// <summary>
    ///   A reference column target.
    /// </summary>
    /// <remarks>A reference column can point to many tables including
    /// the parent table itself. A reference target should be created for each
    /// of those tables and stores additional information
    /// about the need for cascade delete, table back display name etc.</remarks>
    /// <example>
    ///   The following example shows how to create reference column targets.
    /// <code>
    ///     using System;
    ///     using System.Data.Common;
    ///     using Centido.Core;
    /// 
    ///     class Sample
    ///     {
    ///         static void Main()
    ///         {
    ///             var domainConfig = new DomainConfiguration()
    ///             {
    ///                 DbConnectionString = @"Driver={SQL Server};Server=.\SQLEXPRESS;Database=MyDb;Uid=admin;Pwd=P@ssw0rd;",
    ///                 DbProviderName = "MSSQLSERVER"
    ///             };
    ///         
    ///             var domain = Domain.Build(domainConfig);
    ///         
    ///             domain.DeleteAll();
    ///             
    ///             //Adding two tables
    ///             var firstTable = domain.AddTable("first_table");
    ///             var secondTable = domain.AddTable("second_table");
    /// 
    ///             firstTable.GrantAll();
    ///             secondTable.GrantAll();
    ///             
    ///             //Creating a reference column
    ///             var column = firstTable.AddColumn("ref_column", ColumnType.Reference)
    ///                 as RefColumn;
    /// 
    ///             //Adding reference targets
    ///             column.AddTarget(firstTable).HasCascadeDelete = true; //This is a self-reference
    ///             column.AddTarget(secondTable).HasCascadeDelete = true;  
    /// 
    ///             //Inserting some rows
    ///             var secondTableRowId = secondTable.InsertRow();
    ///             var firstTableRowId = firstTable.InsertRow(column.Value(secondTableRowId));
    ///             firstTable.InsertRow(column.Value(firstTableRowId)); //This is a self-reference
    /// 
    ///             //Trying to insert an invalid value
    ///             try
    ///             {
    ///                 firstTable.InsertRow(column.Value(Guid.NewGuid()));
    ///             }
    ///             catch(DbException)
    ///             {
    ///                 Console.WriteLine("Inserting an invalid value is forbidden");
    ///             }
    /// 
    ///             Console.WriteLine("The first table: row count = {0}", firstTable.RowCount);
    ///             Console.WriteLine("The second table: row count = {0}", secondTable.RowCount);
    /// 
    ///             Console.WriteLine("Deleting the first row in the chain of rows");
    /// 
    ///             secondTable.DeleteRow(secondTableRowId);
    /// 
    ///             Console.WriteLine("The first table: row count = {0}", firstTable.RowCount);
    ///             Console.WriteLine("The second table: row count = {0}", secondTable.RowCount);
    ///         }
    ///     }  
    /// </code>
    /// </example>
    public sealed class RefTarget : IRemovable, IRefreshable
    {
        /// <summary>
        ///   The parent reference column.
        /// </summary>
        private readonly RefColumn _column;

        /// <summary>
        ///   The target table.
        /// </summary>
        private readonly Table _refTable;

        /// <summary>
        ///   True if the reference column target has cascade delete.
        /// </summary>
        private bool? _hasCascadeDelete;

        /// <summary>
        ///   Initializes a new instance of the <see cref = "Centido.Core.RefTarget" /> class
        ///   for the specified column and target table.
        /// </summary>
        /// <param name = "column">The parent reference column.</param>
        /// <param name = "refTable">The target table.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="column"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="refTable"/> is null.</exception>
        internal RefTarget(RefColumn column, Table refTable)
        {
            if (column == null)
                throw new ArgumentNullException("column");
            if (refTable == null)
                throw new ArgumentNullException("refTable");

            _column = column;
            _refTable = refTable;
        }

        /// <summary>
        ///   Gets the parent domain.
        /// </summary>
        public Domain Domain
        {
            get { return _column.Domain; }
        }

        /// <summary>
        ///   Gets the parent reference column.
        /// </summary>
        public RefColumn Column
        {
            get { return _column; }
        }

        /// <summary>
        ///   Gets the table that the target points to.
        /// </summary>
        public Table RefTable
        {
            get { return _refTable; }
        }

        /// <summary>
        /// Gets the parent table.
        /// </summary>
        public Table Table
        {
            get { return _column.Table; }
        }

        /// <summary>
        ///   Gets or sets the need to cascade delete rows
        /// that reference the row to be deleted.
        /// </summary>
        /// <remarks>When the data source is the user view then deleting one row may trigger deleting other rows,
        /// if they have a reference on the row to be deleted
        /// and that reference has the CascadeDelete property set to true. If one reference in such a chain
        /// has the CascadeDelete property set to false then the whole transaction is cancelled
        /// and an exception is thrown.
        /// When the data source is the internal table then the deletion is taken place as if
        /// the property CascadeDelete was set to true on all references.</remarks>
        /// <example>
        ///   The following example shows how to delete a chain of rows.
        /// <code>
        ///     using System;
        ///     using Centido.Core;
        /// 
        ///     class Sample
        ///     {
        ///         static void Main()
        ///         {
        ///             var domainConfig = new DomainConfiguration()
        ///             {
        ///                 DbConnectionString = @"Driver={SQL Server};Server=.\SQLEXPRESS;Database=MyDb;Uid=admin;Pwd=P@ssw0rd;",
        ///                 DbProviderName = "MSSQLSERVER"
        ///             };
        ///         
        ///             var domain = Domain.Build(domainConfig);
        ///         
        ///             domain.DeleteAll();
        ///             
        ///             //Adding two tables
        ///             var firstTable = domain.AddTable("first_table");
        ///             var secondTable = domain.AddTable("second_table");
        ///             
        ///             firstTable.GrantAll();
        ///             secondTable.GrantAll();
        /// 
        ///             //Creating a reference column
        ///             var column = firstTable.AddColumn("ref_column", ColumnType.Reference)
        ///                 as RefColumn;
        /// 
        ///             column.AddTarget(firstTable).HasCascadeDelete = true;
        ///             column.AddTarget(secondTable).HasCascadeDelete = true;
        /// 
        ///             //Building a chain of rows
        ///             var secondTableRowId = secondTable.InsertRow();
        ///             var firstTableRowId = firstTable.InsertRow(column.Value(secondTableRowId)); 
        ///             firstTable.InsertRow(column.Value(firstTableRowId));
        /// 
        ///             Console.WriteLine("The first table: row count = {0}", firstTable.RowCount);
        ///             Console.WriteLine("The second table: row count = {0}", secondTable.RowCount);
        /// 
        ///             Console.WriteLine("Deleting the chain of rows");
        /// 
        ///             secondTable.DeleteRow(secondTableRowId);
        /// 
        ///             Console.WriteLine("The first table: row count = {0}", firstTable.RowCount);
        ///             Console.WriteLine("The second table: row count = {0}", secondTable.RowCount);
        ///         }
        ///     }
        /// </code>
        /// </example>
        public bool HasCascadeDelete
        {
            get 
            {
                return _hasCascadeDelete ?? (_hasCascadeDelete =
                    DbValue.GetInteger(ReadProperty(Db.Tables.ReferenceTargetList.Col.HasCascadeDelete)) == 1).Value;
            }
            set 
            {
                if (_hasCascadeDelete == value) return;

                WriteProperty(Db.Tables.ReferenceTargetList.Col.HasCascadeDelete, value ? 1 : 0, OdbcType.Int);

                _hasCascadeDelete = value;
            }
        }

        /// <summary>
        ///   Deletes the reference target.
        /// </summary>
        /// <remarks>
        /// <para>Deleting a reference target results in setting the column to null for those rows that contain
        /// references on the target to be deleted.</para>
        /// <para>The current user has to have the permission to change the schema in order to execute this function.</para>
        /// </remarks>
        /// <exception cref="System.Data.Common.DbException">Thrown when the current user has no permission to execute this function.</exception>
        /// <exception cref="System.Data.Common.DbException">Thrown when there is no such reference target.</exception>
        /// <exception cref="System.Data.Common.DbException">Thrown when the column is not nullable and there are rows that contain
        /// references on the target to be deleted.</exception>
        /// <example>
        ///   The following example shows how to delete a reference target.
        /// <code>
        ///     using System;
        ///     using Centido.Core;
        /// 
        ///     class Sample
        ///     {
        ///         static void Main()
        ///         {
        ///             var domainConfig = new DomainConfiguration()
        ///             {
        ///                 DbConnectionString = @"Driver={SQL Server};Server=.\SQLEXPRESS;Database=MyDb;Uid=admin;Pwd=P@ssw0rd;",
        ///                 DbProviderName = "MSSQLSERVER"
        ///             };
        ///         
        ///             var domain = Domain.Build(domainConfig);
        ///         
        ///             domain.DeleteAll();
        ///             
        ///             //Adding two tables
        ///             var firstTable = domain.AddTable("first_table");
        ///             var secondTable = domain.AddTable("second_table");
        ///             firstTable.GrantAll();
        ///             secondTable.GrantAll();
        ///             
        ///             //Creating a reference column
        ///             var column = firstTable.AddColumn("ref_column", ColumnType.Reference)
        ///                 as RefColumn;
        /// 
        ///             //Adding a reference target
        ///             column.AddTarget(secondTable);
        /// 
        ///             //Inserting a row into the second table
        ///             var secondTableRowId = secondTable.InsertRow();
        /// 
        ///             //Inserting a row into the first table
        ///             var firstTableRowId = firstTable.InsertRow(column.Value(secondTableRowId));
        /// 
        ///             //Reading the reference
        ///             Console.WriteLine("Reference: value = {0}", column.Read(firstTableRowId).GetGuid());
        /// 
        ///             //Deleting the reference target
        ///             Console.WriteLine("Deleting the reference target");
        /// 
        ///             column.DeleteTarget(secondTable);
        /// 
        ///             //Reading the reference
        ///             Console.WriteLine("Reference: value = {0}", column.Read(firstTableRowId).GetGuid());
        ///         }
        ///     }   
        /// </code>
        /// </example>
        public void Delete()
        {
            Column.DeleteTarget(RefTable);
        }

        /// <summary>
        ///   Checks the existence of the reference target.
        /// </summary>
        /// <returns>Returns true if the reference target still exists; otherwise, returns false.</returns>
        public bool Exists()
        {
            var wasFound = false;

            Domain.DbAccess.ExecuteScalar(
                cmd =>
                {
                    cmd.CommandText = string.Format("select count(*) from {0} where {1}=? and {2}=?",
                        Domain.GetObjectFullName(Db.Tables.ReferenceTargetList.Name),
                        Db.Tables.ReferenceTargetList.Col.ColumnId,
                        Db.Tables.ReferenceTargetList.Col.ReferencedTableId);

                    cmd.Parameters.Add(Domain.DbAccess.CreateParameter("column_id", OdbcType.Binary, Column.GetRawId()));
                    cmd.Parameters.Add(Domain.DbAccess.CreateParameter("ref_table", OdbcType.Binary, RefTable.GetRawId()));
                },
                result => wasFound = DbValue.GetInteger(result) != 0);

            return wasFound;
        }

        /// <summary>
        ///   Reads the reference target property.
        /// </summary>
        /// <param name = "propertyName">The property name.</param>
        /// <returns>Returns the property value.</returns>
        private object ReadProperty(string propertyName)
        {
            if (propertyName == null)
                throw new ArgumentNullException("propertyName");
            if (propertyName.Trim().Length == 0)
                throw new ArgumentException("Property name cannot be empty.");

            object resultValue = null;

            Domain.DbAccess.ExecuteScalar(
                cmd =>
                {
                    cmd.CommandText = string.Format(
                        "select {0} from {1} where {2}=? and {3}=?",
                        propertyName,
                        Domain.GetObjectFullName(Db.Tables.ReferenceTargetList.Name),
                        Db.Tables.ReferenceTargetList.Col.ColumnId,
                        Db.Tables.ReferenceTargetList.Col.ReferencedTableId);

                    cmd.Parameters.Add(Domain.DbAccess.CreateParameter("column_id", OdbcType.Binary, Column.GetRawId()));
                    cmd.Parameters.Add(
                        Domain.DbAccess.CreateParameter("ref_table_id", OdbcType.Binary, RefTable.GetRawId()));
                },
                result => resultValue = result);

            return resultValue;
        }

        /// <summary>
        ///   Updates the reference target property.
        /// </summary>
        /// <param name = "propertyName">The property name.</param>
        /// <param name = "propertyValue">The property value.</param>
        /// <param name = "propertyType">The property value database type.</param>
        private void WriteProperty(string propertyName, object propertyValue, OdbcType propertyType)
        {
            if (propertyName == null)
                throw new ArgumentNullException("propertyName");
            if (propertyName.Trim().Length == 0)
                throw new ArgumentException("Property name cannot be empty.");

            var rowCount = Domain.DbAccess.ExecuteNonQuery(
                cmd =>
                {
                    cmd.CommandText = string.Format(
                        "update {0} set {1}=? where {2}=? and {3}=?",
                        Domain.GetObjectFullName(Db.Tables.ReferenceTargetList.Name),
                        propertyName,
                        Db.Tables.ReferenceTargetList.Col.ColumnId,
                        Db.Tables.ReferenceTargetList.Col.ReferencedTableId);

                    cmd.Parameters.Add(
                        Domain.DbAccess.CreateParameter("value", propertyType, DbValue.GetDbValue(propertyValue)));
                    cmd.Parameters.Add(Domain.DbAccess.CreateParameter("column_id", OdbcType.Binary, Column.GetRawId()));
                    cmd.Parameters.Add(
                        Domain.DbAccess.CreateParameter("ref_table_id", OdbcType.Binary, RefTable.GetRawId()));
                });

            if (rowCount < 1)
                throw new DbOperationException();
        }


        /// <summary>
        ///   Refreshes the object fields.
        /// </summary>
        public void Refresh()
        {
            _hasCascadeDelete = null;
        }
    }
}